﻿
module internal Octopus.OctopusService.SchedulerCore

open System
open System.Threading
open Octopus.OctopusLib




let findServerAvailable (memoryRequired:uint64) =
    let nodesList = Database.HyperVNodes.getHyperVNodesReachableWithMemoryFree memoryRequired    
    match nodesList.Length with
    | 0 -> try Database.HyperVNodes.getHyperVNodesUnreachable().[0] with _ -> failwith "No HyperV node available"
    | 1 -> nodesList.[0]
    | _ -> nodesList |> Array.maxBy (fun x -> x.LastMemoryFree)
    


//- - - - - -
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
//- - - -

type TaskRunner (name:string, task:unit -> unit, delay:float, ?avrg:int*float) =
  let mutable _exn : exn = null
  let mutable timeStarted : System.DateTime option = None
  let mutable timeEnded : System.DateTime option = None

  let taskWrapper () =
    timeStarted <- Some System.DateTime.Now
    try task()
    with e -> _exn <- e
    timeEnded <- Some System.DateTime.Now
      
  let thread = new Thread(new ThreadStart(taskWrapper))
 
  member x.Name = name
  member x.Delay = delay
  member x.Exception with get() = _exn
  member x.ThreadState with get() = thread.ThreadState
  member x.TimeStarted with get() = timeStarted
  member x.TimeEnded with get() = timeEnded
  member x.AverageExecutionTime = match avrg with Some(i,t) -> t | None -> 0.
  member x.IsAlive with get() = thread.IsAlive
  member x.IsFinished with get() = timeEnded.IsSome
  
  member x.Start() =
    thread.Start()
    
  member x.ReInstance() =
    let (i,t) = match avrg with Some(s) -> s | None -> 0,0.
    if timeEnded.IsSome then
      let time = (timeEnded.Value - timeStarted.Value).TotalMilliseconds
      let newtime = ((float i) * t + time) / float(i+1)
      new TaskRunner(name,task,delay,(i+1,newtime))
    else
      new TaskRunner(name,task,delay,(i,t))

//----

let adminDomain = Database.GeneralConfig.get_AdminCredentialsDomain()
let adminUsername = Database.GeneralConfig.get_AdminCredentialsUsername()
let adminPassword = Database.GeneralConfig.get_AdminCredentialsPassword()

let task_checkHostAndVm (node:HyperVNode) () =
  // try to get the Hyper-V server performance counters
  try
    let c = new Octopus.OctopusLib.HostNodeWindowsCounters(node.HostName, adminDomain, adminUsername, adminPassword)
    let ipAddr = System.Net.Dns.GetHostAddresses(node.HostName).[0]
    Database.HyperVNodes.updateHyperVNode_reachable node.HostName (int c.CpuCount, int64 c.MemorySize, ipAddr, c.CpuLoad, int64 c.MemoryFree)
    let vms = HyperF.HyperF.get_AllVMSummary (Some node.HostName)
    vms |> Array.iter (fun v -> try FunctionsCore.updateVmState2 v with _ -> ())
       
  // if it fails, set the host as Unreachable and its VMs as HostUnreachable
  with e ->
    Database.LogFile.writeInLog ("-- Exception: " + e.Message)
    Database.LogFile.writeInLog (e.StackTrace)
    Database.LogFile.writePlainText ""
    Database.HyperVNodes.updateHyperVNode_unreachable node.HostName
    let vms = Database.VirtualMachines.getVmByServer node.HostName
    vms |> Array.iter (fun v -> Database.VirtualMachines.updateVirtualMachineHyperVStates v.OctopusGuid None (Some HyperVHealthState.HostUnreachable) None)


let task_configureNewVm () =
  let vmInConfiguration = Database.VirtualMachines.getAllVmInConfigurationState ()
  let vmInConfigurationGrouped = vmInConfiguration |> Seq.ofArray |> Seq.groupBy (fun x -> x.CurrentServer) |> Seq.toArray

  for vmPerHost in vmInConfigurationGrouped do
    try
        let vms = snd vmPerHost |> Seq.toArray
        if vms.Length > 0 then
          let existsVmInConfiguring = vms |> Array.exists (fun x -> x.OctopusState = OctopusVmState.Configuring)
          if not existsVmInConfiguring then
            let vm = vms.[0]
            Database.VirtualMachines.moveVmFrom_ScheduledForConfiguring_To_Configuring_State vm
            let hfvm = HyperF.HyperF.get_VM (Some vm.CurrentServer) (vm.HyperVGuid)
            HyperF.HyperF.start_VM hfvm
    with e ->
        let host = fst vmPerHost
        Database.LogFile.writeInLog (sprintf "Exception in task_configureNewVm[host='%s']: %s" host e.Message)


//----

type ServiceCoreMainLoop () =
  static let mutable _exn : exn = null
  static let mutable timeStarted : System.DateTime option = None
  static let mutable timeEnded : System.DateTime option = None

  static let mutable taskList : TaskRunner array = [||]
  static let mutable isInited = false
  static let mutable stopRequested = false

  static let rec mainLoop() =
    let mutable delayTime = 10.

    Database.LogFile.writeInLog("mainLoop started")
    
    for i = 0 to (taskList.Length - 1) do
      let task = taskList.[i]
      if task.ThreadState = ThreadState.Unstarted then
        task.Start()
        Database.LogFile.writeInLog(sprintf "Task %s started for the first time" task.Name)
      else if task.IsFinished then
        let d = (System.DateTime.Now - task.TimeEnded.Value).TotalSeconds
        if d > (task.Delay-1.) then      // 1 second of tolerance
          if task.Exception <> null then Database.LogFile.writeInLog(sprintf "Task %s throws an exception: %s" task.Name task.Exception.Message)
          taskList.[i] <- task.ReInstance()
          taskList.[i].Start()
          //Database.LogFile.writeInLog(sprintf "Task %s restarted (after %f secs) - Average exec time: %f msecs" taskList.[i].Name d taskList.[i].AverageExecutionTime)
        else
          let d = task.Delay - d
          if d < delayTime then delayTime <- d

    Database.LogFile.writeInLog(sprintf "mainLoop delayTime is %f secs" delayTime)

    try
      let delay = int( delayTime * 1000. )       //minDelay is expressed in seconds
      Thread.Sleep(delay)
    with :? System.Threading.ThreadInterruptedException ->
      Database.LogFile.writeInLog("Received an Interrupt!!")
    
    if stopRequested then Database.LogFile.writeInLog("Stop requested, recursion interrupted.")
    else mainLoop()


  static let thread =
      let taskWrapper () =
        timeStarted <- Some System.DateTime.Now
        try mainLoop()
        with e -> _exn <- e; Database.LogFile.writeInLog("Exception in mainLoop: " + e.Message + "\r\n" + e.StackTrace)
        timeEnded <- Some System.DateTime.Now
        Database.LogFile.writeInLog("DANGER!!! mainLoop Ended!!!")
      new Thread(new ThreadStart(taskWrapper))

  
  static member Exception with get() = _exn
  static member ThreadState with get() = thread.ThreadState
  static member TimeStarted with get() = timeStarted
  static member TimeEnded with get() = timeEnded
  static member IsAlive with get() = thread.IsAlive
  static member IsFinished with get() = timeEnded.IsSome
          
  static member Init() =
    if isInited then failwith "Already inited"
    Clips.initEnv()
    let nodes = Database.HyperVNodes.getHyperVNodes ()
    taskList <- [| for node in nodes -> new TaskRunner("CheckHostAndVm("+node.HostName+")", task_checkHostAndVm node, 10.)
                   yield new TaskRunner("UpdateClips", Clips.task_updateClips, 30.)
                   yield new TaskRunner("ConfigureNewVm", task_configureNewVm, 30.) |]
    isInited <- true

  static member Start() =
    if not(isInited) then failwith "Init the type, first"
    if taskList.Length = 0 then failwith "Nothing to do"
    thread.Start()
    ()

  static member SendInterrupt() =
    if thread.IsAlive then
      thread.Interrupt()
    
  static member Stop() = 
    if thread.IsAlive then
      stopRequested <- true
      thread.Interrupt()

  // TODO: find out why, calling the Join, everything hangs!!
  static member StopWithJoin() =
    if thread.IsAlive then
      stopRequested <- true
      thread.Interrupt()
      thread.Join()
